#!/usr/bin/python
# Zumastor Linux Storage Server
# Copyright (c) 2006 Google Inc.
# Written by Daniel Phillips <phillips@google.com>
# and...
# Licensed under the GNU GPL version 2

import errno
import sys
import os
import commands
import signal
import getopt
import stat

# Friendier mk/rm forms: for mk, return true if the name already
# existed, for rm return true if the name did not exist, otherwise
# throw an exception.  Note the assumption here that if a name
# already exists, it must be the right type of thing, i.e., a
# directory, i.e., it was us who made it.

def mkdir(path):
	try: os.mkdir(path)
	except OSError, (err, str):
		if err == errno.EEXIST:
			return True
		else: raise
	return False

def rmdir(path):
	try: os.rmdir(path)
	except OSError, (err, str):
		if err == errno.ENOENT:
			return True
		else: raise
	return False

def rm(path):
	try: os.remove(path)
	except OSError, (err, str):
		if err == errno.ENOENT:
			return True
		else: raise
	return False

def mkdirtree(path, tree):
    path = path + '/' + tree[0]
    os.system('mkdir ' + path)
    for item in tree[1:]:
        mkdirtree(path, item)
	
def rmdirtree(path, tree):
    path = path + '/' + tree[0]
    for item in tree[1:]:
        rmdirtree(path, item)
    os.system('rmdir ' + path)
	
def rmtree(path):
	commands.getstatusoutput("rm -r '" + path + "'")

def make(file):
	open(file,'a')

def listdir(path):
	return [name for name in os.listdir(path) if name[0] != '.']

def readfile(name):
	try: return open(name, 'r').read()
	except IOError, (err, str):
		if err == errno.ENOENT:
			return ""
		else: raise

def writefile(name, text):
	open(name, 'w').write(text)

def nint(string):
	return len(string) and int(string) or 0 # lame

def pkill(pattern): # lose me
	print commands.getstatusoutput('pkill -f "' + pattern + '$"')

# and without further ado... #

def ints_to_string(list):
	return ' '.join([`item` for item in list])

def string_to_ints(string):
	return [int(item) for item in string.split()]

dbpath = '/var/lib/zumastor/volumes/'
runpath = '/var/run/zumastor/'
kinds = ['hourly', 'daily', 'weekly', 'monthly']

rundirs = ['zumastor',
	['agents'],
	['cron'],
	['mount'],
	['pid', ['master'], ['target'], ['ddsnap']],
	['servers'],
	['trigger', ['master'], ['target']]]

def ddsnap(args):
	status, output = commands.getstatusoutput('echo ddsnap ' + ' '.join(args))
	print output
	return status

def vol_path(vol):
	return dbpath + vol + '/'

def target_path(vol):
	return vol_path(vol) + 'targets/'

def master_path(vol):
	return vol_path(vol) + 'master/'

def snap_path(vol):
	return master_path(vol) + 'snapshots/'

def master_trigger(vol):
	return runpath + 'trigger/master/' + vol

def target_trigger_path(vol):
	return runpath + 'trigger/target/' + vol + '/'

def master_pidfile(vol):
	return runpath + 'pid/master/' + vol

def agent_sock(vol):
	return runpath + 'pid/agents/' + vol

def server_sock(vol):
	return runpath + 'pid/server/' + vol

def target_pidpath(vol):
	return runpath + 'pid/target/' + vol + '/'

def target_pidfile(vol, host):
	return target_pidpath(vol) + host

def running():
	return os.path.exists(runpath + 'pid')

def is_master(vol):
	return os.path.exists(master_path(vol))

def is_target(vol):
	return os.path.exists(vol_path(vol) + 'source')

def valid_kind(kind):
	return kind in kinds

def master_snaps(vol):
	return string_to_ints(' '.join([readfile(snap_path(vol) + name)
		for name in listdir(snap_path(vol))]))

def snapdev(vol, snap):
	return vol + '(' + `snap` + ')'

def create_device(vol, snap):
	print 'create device', snapdev(vol, snap)

def mount_snap(vol, snap):
	mkdir(runpath + 'mount/' + snapdev(vol, snap))
	# create dm snap device
	# mount dm snap device

def unmount_snap(vol, snap):
	rmdir(runpath + 'mount/' + snapdev(vol, snap))
	# create dm snap device
	# mount dm snap device

def new_snapshot(vol, kind):
	print 'new snapshot', vol, kind
	if (not valid_kind(kind)): return
	path = master_path(vol)
	snap = nint(readfile(path + 'next'))
	lim = nint(readfile(path + 'schedule/' + kind))
	if not lim:
		print 'no limit'
		return
	file = path + 'snapshots/' + kind
	# this is wrong...
	err = ddsnap(['snapshot', `snap`, agent_sock(vol), server_sock(vol)])
	mkdir(runpath + 'mount/' + snapdev(vol, snap))
	mount_snap(vol, snap)
	if err:
		print 'ddsnap error', err
		return
	snaps = string_to_ints(readfile(file))
	snaps = snaps + [snap]
	if len(snaps) > lim:
		unmount_snap(vol, snaps[0])
		snaps = snaps[1:]
	print snaps
	writefile(file, ints_to_string(snaps))
	writefile(path + 'next', `snap + 1`)

def start_master(vol):
	if not running():
		return
	os.mkfifo(master_trigger(vol))
	for snap in master_snaps(vol):
		mount_snap(vol, snap)
	pid = os.fork()
	if pid:
		writefile(master_pidfile(vol), `pid`)
		return
	print 'master daemon', vol
	while 1:
		pipe = open(master_trigger(vol), 'r')
		kind = pipe.read(99).strip()
		pipe.close
		new_snapshot(vol, kind)

def pidfile_kill(pidfile):
	pid = 0
	try:
		pid = int(readfile(pidfile))
		os.kill((pid), signal.SIGTERM)
	except: print 'did not kill', pidfile, readfile(pidfile)
	rm(pidfile)

def start_target(vol, host):
	if not running():
		return
	mkdir(target_pidpath(vol))
	mkdir(target_trigger_path(vol))
	mkdir(target_pidpath(vol))
	os.mkfifo(target_trigger_path(vol) + host)
	pid = os.fork()
	if pid:
		writefile(target_pidfile(vol, host), `pid`)
		return
	while 1:
		pipe = open(target_trigger_path(vol) + host, 'r')
		kind = pipe.read(99).strip()
		pipe.close
		print 'replicate', vol, host

def stop_master(vol):
	if not running():
		return
	pidfile_kill(master_pidfile(vol))
	rm(master_trigger(vol))
	for snap in master_snaps(vol):
		unmount_snap(vol, snap)

def start_source(vol):
	if not running():
		return
	print 'start source', vol

def start_volume(vol):
	if not running():
		return
	mkdir(runpath + 'mount/' + vol)
	# create dm org device
	# mount dm org device
	if is_target(vol):
		start_source(vol)
	if is_master(vol):
		start_master(vol)
	for host in listdir(target_path(vol)):
		start_target(vol, host)

def stop_source(vol):
	if not running():
		return
	print 'stop source', vol

def stop_target(vol, host):
	if not running():
		return
	pidfile_kill(target_pidfile(vol, host))
	rmdir(target_pidpath(vol))
	rmtree(target_trigger_path(vol))

def stop_volume(vol):
	if not running():
		return
	for host in listdir(target_path(vol)):
		stop_target(vol, host)
	if is_master(vol):
		stop_master(vol)
	if is_target(vol):
		stop_source(vol)
	# unmount dm org device
	# remove dm org device
	rmdir(runpath + 'mount/' + vol)

def forget_master(vol):
	rmtree(master_path(vol))
	rmdir(target_trigger_path(vol))

def forget_target(vol, host):
	print 'forget target', vol, hos
	path = target_path(vol) + host + '/'
	rmdir(path)

def forget_source(vol):
	path = vol_path(vol)
	rm(path + 'source')

def forget_volume(vol):
	for host in listdir(target_path(vol)):
		forget_target(vol, host)
	if is_master(vol):
		forget_master(vol)
	if is_target(vol):
		forget_source(vol)
	rmtree(vol_path(vol))

# shell commands #

def bail(message):
	print message
	sys.exit(1)

def must_be_running():	
	if not running():
		bail('Zumastor is not started')

def try_help():
	print "try", sys.argv[0], "help"

def need_args(n, args):
	if len(args) < n:
		print 'too few args'
		sys.exit()

def define_volume_cmd(args):
	need_args(1, args)
	vol = args[0]
	path = vol_path(vol)
	if mkdir(path):
		print vol, 'already exists'
		return
	mkdir(path + 'device')
	mkdir(path + 'targets')
	start_volume(vol)
	create_device(vol, -1)

def is_number(x):
    import re 
    num_re = re.compile(r'^[-+]?([0-9]+\.?[0-9]*|\.[0-9]+)([eE][-+]?[0-9]+)?$')
    return str(re.match(num_re, x)) != 'None'

def define_master_cmd(args):
	opts, args = getopt.gnu_getopt(args, 'h:d:w:m',  ['hourly=', 'daily=', 'weekly=', 'monthly='])
	need_args(1, args)
	vol = args[0]
	stop_source(vol)
	forget_source(vol)
	path = master_path(vol)
	existing = mkdir(path)
	if not existing:
		mkdir(path + 'schedule')
		mkdir(path + 'snapshots')
	print opts
	opts = [[{
		'-h': 'hourly', '--hourly': 'hourly',
		'-d': 'daily', '--daily': 'daily',
		'-w': 'weekly', '--weekly': 'weekly',
		'-m': 'monthly', '--monthly': 'monthly'
	}[opt[0]], opt[1]] for opt in opts]
	for opt in opts:
		print 'kind:', opt[0], opt[1]
		if not is_number(opt[1]):
			bail(opt[0] + ' option requires a number')
	for opt in opts:
		kind = opt[0]
		writefile(path + 'schedule/' + kind, opt[1])
		make(path + 'snapshots/' + kind)
	if not existing:
		start_master(vol)

def define_source_cmd(args):
	need_args(2, args)
	vol = args[0]
	host = args[1]
	stop_master(vol)
	forget_master(vol)
	path = vol_path(vol)
	writefile(path + 'source', host)

def define_target_cmd(args):
	need_args(2, args)
	vol = args[0]
	host = args[1]
	path = target_path(vol) + host + '/'
	existing = mkdir(path)
	if not existing:
		start_target(vol, host)

def forget_volume_cmd(args):
	need_args(1, args)
	vol = args[0]
	stop_volume(vol)
	forget_volume(vol)

def forget_source_cmd(args):
	need_args(1, args)
	forget_source(args[0])

def forget_target_cmd(args):
	need_args(2, args)
	forget_target(args[0], args[1])

def start_master_cmd(args):
	need_args(1, args)
	start_master(args[0])

def start_source_cmd(args):
	need_args(1, args)
	start_source(args[0])

def start_target_cmd(args):
	need_args(2, args)
	start_target(args[0], args[1])

def stop_master_cmd(args):
	need_args(1, args)
	stop_master(args[0])

def stop_source_cmd(args):
	need_args(1, args)
	stop_source(args[0])

def stop_target_cmd(args):
	need_args(2, args)
	stop_target(args[0], args[1])

def snapshot_cmd(args):
	need_args(2, args)
	vol = args[0]
	kind = args[1]
	must_be_running()
	if os.path.exists(master_trigger(vol)) and valid_kind(kind):
		open(master_trigger(vol), 'w').write(kind)

def replicate_cmd(args):
	need_args(2, args)
	vol = args[0]
	host = args[1]
	must_be_running()
	trigger = target_trigger_path(vol) + host
	if os.path.exists(trigger):
		open(trigger, 'w').write('123')

def receive_cmd(args):
	print 'receive'

def listdir(path):
	return [name for name in os.listdir(path) if name[0] != '.']

def treelist(root, flags):
	showhidden, showfull = flags & 1, flags & 2
	path, names, cursor, level = '', [[root]], [0], 0
	while names:
		while cursor[level] < len(names[level]):
			name = names[level][cursor[level]]
			cursor[level] += 1
			fullname = os.path.join(path, name)
			try: mode = (level == 0 and os.stat or os.lstat)(fullname).st_mode
			except OSError, (err, str):
				if err != errno.ENOENT: raise
				print fullname, 'does not exist';
				sys.exit(1)
			prefix = ''
			for i in range(1, level + 1):
				prefix += [['|   ', '    '], ['|-- ', '`-- ']] \
					[i == level][cursor[i] == len(names[i])]
			if stat.S_ISDIR(mode):
				path = fullname
				if showhidden:
					newnames = os.listdir(path)
				else:
					newnames = [crap for crap in os.listdir(path) if crap[0] != '.']
				newnames.sort()
				names += [newnames]
				cursor += [0]
				level += 1
				print prefix + name
			elif stat.S_ISLNK(mode):
				print prefix + name, '->', os.readlink(fullname)
			elif stat.S_ISREG(mode):
				# limit size and translate bin to hex!!!
				value = open(fullname, 'r').read()
				print prefix + name + ' = ' + value
			elif stat.S_ISFIFO(mode):
				print prefix + name + '='
			else:
				print prefix + name + ' ??'
		del names[level]
		del cursor[level]
		path = os.path.split(path)[0]
		level -= 1

def status_cmd(args):
	opts, args = getopt.gnu_getopt(args, 'f')
	o = ' '
	if len(opts) and opts[0][0] == '-f':
		o += '-f'
	treelist('/var/lib/zumastor', 0)
	treelist('/var/run/zumastor', 0)
	#os.system('tree' + o + ' /var/lib/zumastor')
	#os.system('tree' + o + ' /var/run/zumastor')
	#print 'snaps', [[vol, master_snaps(vol)] for vol in listdir(dbpath) if is_master(vol)]

def help_cmd(args):
	print '(under construction)'

def unknown_cmd(args):
	print ' '.join(sys.argv[1:]), "is not a command"
	try_help()

def cmd(args, mapping):
	need_args(1, args)
	try: mapping.get(args[0], unknown_cmd)(args[1:])
	except getopt.GetoptError, (err, str):
		print str, "is not an option"
		try_help()

def define_cmd(args):
	cmd(args, {
	'volume': define_volume_cmd,
	'master': define_master_cmd,
	'source': define_source_cmd,
	'target': define_target_cmd})

def forget_cmd(args):
	cmd(args, {
	'volume': forget_volume_cmd,
	'source': forget_source_cmd,
	'target': forget_target_cmd})

def start_cmd(args):
	if not args:
		mkdirtree('/var/run', rundirs)
		for vol in listdir(dbpath):
			start_volume(vol)
		sys.exit(0)
	cmd(args, {
	'master': start_master_cmd,
	'source': start_source_cmd,
	'target': start_target_cmd})

def stop_cmd(args):
	if not args:
		for vol in listdir(dbpath):
			stop_volume(vol)
		rmdirtree('/var/run', rundirs)
		sys.exit(0)
	cmd(args,{
	'master': stop_master_cmd,
	'source': stop_source_cmd,
	'target': stop_target_cmd})

cmd(sys.argv[1:], {
	'define': define_cmd,
	'forget': forget_cmd,
	'start': start_cmd,
	'stop': stop_cmd,
	'snapshot': snapshot_cmd,
	'replicate': replicate_cmd,
	'receive': receive_cmd,
	'status': status_cmd,
	'--help': help_cmd,
	'help': help_cmd})
